// Copyright 2011 Google Inc. All Rights Reserved.

// Package imc implements the IMC protocol
// http://www.chromium.org/nativeclient/reference/imc
package imc

import (
	"encoding/binary"
	"fmt"
	"io"
	"os"
	"syscall"

	"naclrun.googlecode.com/git/pkg/udrpc"
)

const (
	// Magic for current version of IMC.
	Protocol uint32 = 0xd3c0de01
	Padding         = 16

	// IMC handle code for regular file.
	FileTag = 2

	// IMC handle code for shared memory handle.
	ShmTag = 7

	// A special handle code that means 'no more handles'
	EofTag = 0xFF
)

// Socket is an IMC socket.
type Socket struct {
	s *udrpc.Socket
}


type header struct {
	Protocol uint32
	Length   uint32
	Padding  uint64
}

// Close closes the Socket, rendering it unusable for I/O.
func (s *Socket) Close() os.Error {
	return s.s.Close()
}

func sysLen(length int) int {
	if length == 0 {
		return 0
	}
	return (length + 1 + Padding) & ^(Padding - 1)
}

func buildMessageHeader(buf *udrpc.Buffer) {
	h := header{
		Protocol: Protocol,
		Length:   uint32(sysLen(buf.Handles.Len())),
	}
	binary.Write(buf.Buffer, binary.LittleEndian, h)
	sig := make([]byte, h.Length)
	for i := 0; i < buf.Handles.Len(); i++ {
		sig[i] = FileTag
	}
	if h.Length > 0 {
		sig[buf.Handles.Len()] = EofTag
	}

	binary.Write(buf.Buffer, binary.LittleEndian, sig)
}

// Send prepares and sends the data + handles to the other side of socket pair.
func (s *Socket) Send(data []byte, handles *udrpc.Handles) (err os.Error) {
	if handles == nil {
		handles = new(udrpc.Handles)
	}
	buf := udrpc.NewBuffer(handles)
	buildMessageHeader(buf)
	buf.Buffer.Write(data)
	return s.s.NonBlockingWrite(buf)
}

func checkMetadata(r io.Reader, handleCount int) (err os.Error) {
	var hdr header
	err = binary.Read(r, binary.LittleEndian, &hdr)
	if err != nil {
		return fmt.Errorf("imc: could not read header: %v", err)
	}
	if hdr.Protocol != Protocol {
		return fmt.Errorf("imc: unexpected protocol, want: %v, got: %v", Protocol, hdr.Protocol)
	}
	if hdr.Padding != 0 {
		return os.NewError("imc: padding in imc header must be zero, but it's not")
	}
	if hdr.Length == 0 && handleCount > 0 {
		return os.NewError("imc: handle metadata has not been passed")
	}
	if hdr.Length > 0 && handleCount == 0 {
		return fmt.Errorf("imc: no handles have been passed, but there's metadata for %d handles", hdr.Length)
	}
	if handleCount > 0 && hdr.Length != uint32(sysLen(handleCount)) {
		return fmt.Errorf("imc: metadata length: %d does not match to the expected len: %d", hdr.Length, sysLen(handleCount))
	}
	if handleCount == 0 {
		return
	}
	tags := make([]byte, handleCount)
	err = binary.Read(r, binary.LittleEndian, tags)
	if err != nil {
		return fmt.Errorf("imc: can't read handle tags (%d expected): %v", handleCount, err)
	}
	for _, tag := range tags {
		if tag != FileTag && tag != ShmTag {
			return fmt.Errorf("imc: unsupported tag: %d", tag)
		}
	}
	padding := make([]byte, sysLen(handleCount)-handleCount)
	err = binary.Read(r, binary.LittleEndian, padding)
	return
}

// Recv reads and validates a message from the socket pair.
func (s *Socket) Recv() (data []byte, handles *udrpc.Handles, err os.Error) {
	buf, err := s.s.BlockingRead()
	if err != nil {
		return
	}
	h := buf.Handles
	if err = checkMetadata(buf.Buffer, h.Len()); err != nil {
		h.Close()
		return
	}
	return buf.Buffer.Bytes(), h, nil
}

// NewSocket returns a new socket corresponding to the given file.
func NewSocket(f *os.File) *Socket {
	return &Socket{udrpc.NewSocket(f)}
}

// SocketPair returns a socket pair to be used for communicating
// with NaCl module.
func SocketPair() (f0, f1 *os.File, err os.Error) {
	fd, errno := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_SEQPACKET, 0)
	if errno != 0 {
		err = os.NewSyscallError("socketpair", errno)
		return
	}
	return os.NewFile(fd[0], "imc-socket-0"), os.NewFile(fd[1], "imc-socket-1"), nil
}
